﻿using System;
using System.Collections.Generic;
using System.Data.Linq;
using System.Data.Linq.Mapping;
using System.Linq;
using System.Linq.Expressions;
using System.Transactions;

namespace DotNetRepository.LinqToSql
{
    public sealed class LinqToSqlRepository<TEntity> : BaseRepository<TEntity>, IEntityRepository<TEntity> where TEntity : class, IEntity
    {
        public static readonly LinqToSqlRepository<TEntity> Instance = new LinqToSqlRepository<TEntity>();
        private LinqToSqlRepository() { }

        private static readonly MappingSource MappingSource = new AttributeMappingSource();
        private static readonly string ConnectionString = Settings.Default.DefaultConnectionString;
        private static readonly Type DataContextType = Type.GetType(Settings.Default.DefaultDataConext);

        private readonly EntityCache<TEntity> _cache = EntityCache<TEntity>.Instance;

        private static readonly Func<DataContext, Guid, TEntity> GetEntityByID = CompiledQuery.Compile(
            (DataContext dc, Guid id) => dc.GetTable<TEntity>().FirstOrDefault(o => o.ID == id));

        protected override IEntityRepository<TEntity> Self
        {
            get { return this; }
        }

        public override TEntity GetByID(Guid id)
        {
            if (id == Guid.Empty) return null;
            GuidLock.Lock(id);
            try
            {
                var item = _cache.GetData(id);

                if (item == null)
                {
                    var context = GetNewDataContext();
                    var entity = GetEntityByID(context, id);
                    if (entity == null)
                    {
                        throw new ArgumentOutOfRangeException(
                            String.Format("There is no object of type {0} with ID {1}.", typeof(TEntity).Name, id));
                    }
                    
                    InitializeEntity(entity);

                    item = new EntityCacheItem<TEntity>(entity, context);
                    _cache.Add(item);
                }

                return item.Entity;
            }
            finally
            {
                GuidLock.UnLock(id);
            }
        }

        public override TEntity GetCachedByID(Guid id)
        {
            if (id == Guid.Empty) return null;
            GuidLock.Lock(id);
            try
            {
                var item = _cache.GetData(id);
                return item == null ? null : item.Entity;
            }
            finally
            {
                GuidLock.UnLock(id);
            }
        }

        /// <summary>
        /// Gets the cached object with the matching ID if it exists in memory.  If not,
        /// caches the object passed in, creates a new data context for it, and then returns it.
        /// </summary>
        /// <param name="entity"></param>
        /// <returns></returns>
        private TEntity GetEntity(TEntity entity)
        {
            GuidLock.Lock(entity.ID);

            var item = _cache.GetData(entity.ID);

            try
            {
                if (item == null)
                {
                    var context = GetNewDataContext();
                    context.GetTable<TEntity>().Attach(entity);
                    item = new EntityCacheItem<TEntity>(entity, context);
                    _cache.Add(item);
                }
            }
            finally
            {
                GuidLock.UnLock(entity.ID);
            }

            return item.Entity;
        }

        /// <summary>
        /// Returns true if changes have been made to an object that have not yet been saved.
        /// </summary>
        /// <param name="entity"></param>
        /// <returns></returns>
        public override bool IsDirty(TEntity entity)
        {
            // Ensure that no further changes are made to an object until finished with this operation
            lock (entity)
            {
                var item = _cache.GetData(entity.ID);
                return item == null || item.Context.GetChangeSet().Updates.Contains(entity);
            }
        }

        public override void Save(TEntity entity, object sender)
        {
            // Ensure that no further changes are made to an object until after it is saved.
            lock (entity)
            {
                EntityCacheItem<TEntity> item = null;
                DataContext context;

                if (entity.Version != 0) // existing entity
                {
                    item = _cache.GetData(entity.ID);
                    context = item.Context;
                }
                else // new entity
                {
                    context = GetNewDataContext();
                    context.GetTable<TEntity>().InsertOnSubmit(entity);
                }

                InitializeEntity(entity);

                var anythingToSave = (item == null || context.GetChangeSet().Updates.Contains(entity));

                try
                {
                    if (HasEntityCollections || HasChildEntitiesToPersist)
                    {
                        var scopeOptions = new TransactionOptions { IsolationLevel = IsolationLevel.ReadUncommitted };
                        var scope = new TransactionScope(TransactionScopeOption.Required, scopeOptions);

                        using (scope)
                        {

                            if (anythingToSave)
                            {
                                entity.IncrementVersion();
                                context.SubmitChanges();
                            }

                            // Save all related entity collections
                            SaveCollections(entity);
                            SaveChildEntities(entity);
                            scope.Complete();
                        }
                    }
                    else if (anythingToSave)
                    {
                        entity.IncrementVersion();
                        context.SubmitChanges();
                    }
                }
                catch (Exception e)
                {
                    if (item != null) Revert(entity);
                    throw new ApplicationException(string.Format("Error saving {0} ({1}) to database.", typeof(TEntity).Name, entity.ID), e);
                }

                if (item == null)
                {
                    item = new EntityCacheItem<TEntity>(entity, context);
                    _cache.Add(item);
                }
                if (anythingToSave && HasEnumerationsToNotify)
                    NotifyEntityUpdated(entity, sender);
            }
        }

        public override void Revert(TEntity entity)
        {
            lock (entity)
            {
                // Check to see if the object has ever been saved
                if (_cache.Contains(entity.ID))
                {
                    var dc = _cache.GetData(entity.ID).Context;
                    // Revert to the values on the database
                    dc.Refresh(RefreshMode.OverwriteCurrentValues, entity);
                }

                RevertCollections(entity);
            }
        }

        public override void Delete(TEntity entity, object sender)
        {
            // Ensure that the object is not changed, saved, or deleted again before
            // this delete completes.
            lock (entity)
            {
                var item = _cache.GetData(entity.ID);
                if (item == null) return;

                item.Context.GetTable<TEntity>().DeleteOnSubmit(entity);

                try
                {
                    if (HasChildEntitiesToDelete || HasEntityCollections)
                    {
                        var scopeOptions = new TransactionOptions { IsolationLevel = IsolationLevel.ReadUncommitted };
                        var scope = new TransactionScope(TransactionScopeOption.Required, scopeOptions);
                        using (scope)
                        {
                            DeleteChildEntities(entity);
                            ClearEntityCollections(entity);
                            item.Context.SubmitChanges();

                            scope.Complete();
                        }
                    }
                    else
                    {
                        //DeleteChildEntities(entity);
                        //ClearEntityCollections(entity);
                        item.Context.SubmitChanges();
                    }
                    NotifyEntityDeleted(item.Entity, sender);
                    _cache.ForceRemove(entity);
                }
                catch (Exception ex)
                {
                    Revert(entity);
                    throw new ApplicationException(string.Format("Error deleting {0} ({1})!", typeof(TEntity).Name, entity.ID), ex);
                }
            }
        }

        public override bool IsCached(Guid id)
        {
            return _cache.Contains(id);
        }

        public override bool Contains(TEntity entity)
        {
            return _cache.Contains(entity.ID);
        }

        /// <summary>
        /// Get a readonly view of a table for querying data on the database.
        /// </summary>
        /// <param name="context">Represents a DataContext.  Queries deriving from more than one table must
        /// use the same context.</param>
        /// <returns></returns>
        public IEnumerable<TEntity> GetTable(ref object context)
        {
            var dc = context as DataContext;
            if (dc == null)
            {
                dc = GetNewDataContext();
                dc.ObjectTrackingEnabled = false;
                context = dc;
            }

            return dc.GetTable<TEntity>();
        }

        public IEnumerable<T> GetTable<T>(ref object context) where T : class, IEntity
        {
            var dc = context as DataContext;
            if (dc == null)
            {
                dc = GetNewDataContext();
                dc.ObjectTrackingEnabled = false;
                context = dc;
            }

            return dc.GetTable<T>();
        }

        public override bool Any(Expression<Func<TEntity, bool>> predicate)
        {
            var dc = GetNewDataContext();
            dc.ObjectTrackingEnabled = false;

            return
                dc.GetTable<TEntity>()
                    .Where(predicate)
                    .Count() > 0;
        }

        public override List<Guid> GetIDs(Expression<Func<TEntity, bool>> predicate)
        {
            var dc = GetNewDataContext();
            dc.ObjectTrackingEnabled = false;

            return dc.GetTable<TEntity>()
                .Where(predicate)
                .Select(o => o.ID)
                .ToList();
        }

        public override List<TEntity> Where(Expression<Func<TEntity, bool>> predicate)
        {
            var dc = GetNewDataContext();
            dc.ObjectTrackingEnabled = false;
            
            return 
                dc.GetTable<TEntity>()
                .Where(predicate)
                .Select(GetEntity)
                .ToList();
        }

        public List<TEntity> GetEntitiesByStoredProcedure(Func<DataContext,ISingleResult<TEntity>> method)
        {
            var dc = GetNewDataContext();
            var entities = method(dc);
            return entities.Select(GetEntity).ToList();
        }

        public override TEntity FirstOrDefault(Expression<Func<TEntity, bool>> filter)
        {
            var dc = GetNewDataContext();
            dc.ObjectTrackingEnabled = false;

            var obj = dc.GetTable<TEntity>()
                .Where(filter)
                .OrderBy(o => o.ID)
                .FirstOrDefault();

            return obj == null ? null : GetEntity(obj);
        }

        public override TEntity LastOrDefault(Expression<Func<TEntity, bool>> filter)
        {
            var dc = GetNewDataContext();
            dc.ObjectTrackingEnabled = false;

            var obj = dc.GetTable<TEntity>()
                .Where(filter)
                .OrderByDescending(o => o.ID)
                .FirstOrDefault();

            return obj == null ? null : GetEntity(obj);
        }

        private static DataContext GetNewDataContext()
        {
            //var dc = new DataContext(ConnectionString, MappingSource);
            var dc = Activator.CreateInstance(DataContextType, ConnectionString, MappingSource) as DataContext;
            dc.Log = Settings.Default.EnableDataContextTrace ? Console.Out : null;
            return dc;
        }

        //protected override Type GetGenericRepositoryType(IEntity entity)
        //{
        //    return typeof(LinqToSqlRepository<>).MakeGenericType(new[] { entity.GetType() });
        //}

        //protected override Type GetGenericRepositoryType<T>() 
        //{
        //    return typeof(LinqToSqlRepository<>).MakeGenericType(new[] { typeof(T) });
        //}

    }
}
